package apibasej.xml;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.parsers.ParserConfigurationException;

import org.xml.sax.SAXException;

import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import apibasej.basic.exception.APIBaseException;
import apibasej.basic.misc.UtilStrObj;
import apibasej.basic.misc.standalone.UtilReflectionField;

@SuppressWarnings({"unchecked","rawtypes"})
public class DataXML{ //extends ConfigConsumer

	public static final String TAG_ITEM = "item";
	public static final String ATR_KEY = "key"; // o value será sempre o texto do nó, se o item tiver key será um item de um map, senão de uma lista
	
	public static final String ATR_TYPE_MAP = "typeMap";
	public static final String ATR_TYPE_KEY = "typeKey";
	public static final String ATR_TYPE_VALUE = "typeValue";
	public static final String ATR_TYPE_LIST = "typeList";
	
	//private enum TypeVal{MAP,LIST,STRING} // todos os itens da lista ou do map devem ter o mesmo valor
	
	private boolean addEmptys = false;
	
	public DocXML toXML(Object obj) throws ParserConfigurationException, SAXException, IOException, IllegalArgumentException, IllegalAccessException, UtilXMLException, InvocationTargetException, SecurityException, NoSuchMethodException{
		DocXML d = new DocXML();
		ElemXML root = d.createRoot( getNameTagForClass(obj.getClass()) );
		fillElem(obj, root, null);
		return d;
	}
	// cria a tag XML para este objeto no elemFather
	public void toXML(Object obj, ElemXML elemFather) throws ParserConfigurationException, SAXException, IOException, IllegalArgumentException, IllegalAccessException, UtilXMLException, InvocationTargetException, SecurityException, NoSuchMethodException{
		ElemXML el = elemFather.createChild( getNameTagForClass(obj.getClass()) );
		fillElem(obj, el, null);
	}
	
	public <T> T toObj(DocXML docXML, Class<T> typeObj) throws InstantiationException, IllegalAccessException, UtilXMLException, IllegalArgumentException, InvocationTargetException, SecurityException, NoSuchMethodException, NoSuchFieldException, ParseException, ClassNotFoundException, APIBaseException, IOException{
		return toObj(docXML, typeObj, null);
	}
	public <T> T toObj(DocXML docXML, Class<T> typeObj, Object instObjFather) throws InstantiationException, IllegalAccessException, UtilXMLException, IllegalArgumentException, InvocationTargetException, SecurityException, NoSuchMethodException, NoSuchFieldException, ParseException, ClassNotFoundException, APIBaseException, IOException{
		return readElem(docXML.getElemRoot(), typeObj, null, instObjFather);
	}
	public <T> T toObj(ElemXML elFather, Class<T> typeObj) throws InstantiationException, IllegalAccessException, UtilXMLException, IllegalArgumentException, InvocationTargetException, SecurityException, NoSuchMethodException, NoSuchFieldException, ParseException, ClassNotFoundException, APIBaseException, IOException{
		ElemXML el = elFather.getFirstElemByTagName(getNameTagForClass(typeObj), false, true);
		return readElem(el, typeObj, null, null);
	}
	
	
	public static <T> T clone(T obj) throws Exception{
		if(obj==null){
			return null;
		}else{
			DataXML d = new DataXML();
			DocXML dx = d.toXML(obj);
			return d.toObj(dx, (Class<T>)obj.getClass());
		}
	}
	
	private String getNameTagForField(Field f){
		NodeXML an = f.getAnnotation(NodeXML.class);
		String nameTag = an.value();
		if(nameTag==null || nameTag.trim().length()==0) nameTag = f.getName();
		return nameTag;
	}
	
	private String getNameTagForClass(Class<?> c) throws UtilXMLException{
		// não validar ???
		if(!c.isAnnotationPresent(NodeXML.class)) throw new UtilXMLException("Class must have Annotation NodeXML: "+c.getName());
		NodeXML a = c.getAnnotation(NodeXML.class);
		String n = a==null?null:a.value();
		if(n==null || n.trim().length()==0) n = c.getSimpleName();
		return n;
	}
	private String getNameTagForItemOfListOrMap(Field fieldListOrMap){
		if(fieldListOrMap==null) return TAG_ITEM;
		NodeXML an = fieldListOrMap.getAnnotation(NodeXML.class);// deve estar validado se tem a annotation neste momento
		String nameTagItem = an.nameTagItem();
		if(nameTagItem==null || nameTagItem.trim().length()==0) nameTagItem = TAG_ITEM;
		return nameTagItem;
	}

	
	private void fillElemObj(Object obj, ElemXML elemObj) throws IllegalArgumentException, IllegalAccessException, UtilXMLException, InvocationTargetException, SecurityException, NoSuchMethodException{
		//validateAnnClass(obj.getClass()); // precisa???
		Class<?> c = obj.getClass();
		while(c.isAnnotationPresent(NodeXML.class)){ // para verificar os fields das superclasses que tiverem a anotação ...
			for(Field f : c.getDeclaredFields()){
				//if(Modifier.isFinal(f.getModifiers())) continue; // se for final não preenche
				if(f.isAnnotationPresent(NodeXML.class)){
					
					Object v = new UtilReflectionField(f).getValueFromObject(obj); //f.get(obj);
					if(v==null) continue;
					
					NodeXML ann = f.getAnnotation(NodeXML.class);
					if(ann!=null && ann.asAttribute()){
						elemObj.setAttribute(getNameTagForField(f), UtilStrObj.toStr(v));
					}else{
						ElemXML el = elemObj.createChild(getNameTagForField(f));
						if(ann!=null && ann.needType() && !f.getType().equals(v.getClass())) { // se tem o needType, mas o tipo do objeto é igual do field, não precisa setar o tipo
							el.setAttribute(ATR_TYPE_VALUE, v.getClass().getName());
						}
						fillElem(v, el, f);
					}

				}
			}
			c = c.getSuperclass(); // para verificar os fields da superclasse
		}
	}
	
	
	// dar preferencia para LinkedHashMap por causa da ordem
	private void fillElemMap(Map<?,?> map, ElemXML el, Field f) throws UtilXMLException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException{
		if(map==null || map.size()==0) return;
		for(Entry<?,?> e : map.entrySet()){
			Object v = e.getValue();
			if(v==null) {
				continue;
			}
			String keyStr = UtilStrObj.toStr(e.getKey());
			ElemXML ch = el.createChild(getNameTagForItemOfListOrMap(f));
			ch.setAttribute(ATR_KEY, keyStr);
			if(f==null) {
				ch.setAttribute(ATR_TYPE_KEY, e.getKey().getClass().getName());
			}
			if(f==null || f.getAnnotation(NodeXML.class).needType()) {
				ch.setAttribute(ATR_TYPE_VALUE, v.getClass().getName());
			}
			fillElem(v, ch, null);
		}
	}
	// dar preferencia para ArrayList por causa da ordem
	private void fillElemList(Collection list, ElemXML el, Field f) throws UtilXMLException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException{
		if(list==null || list.size()==0) return;
		for(Object v : list){
			if(v==null) {
				continue;
			}
			ElemXML ch = el.createChild(getNameTagForItemOfListOrMap(f));
			if(f==null || f.getAnnotation(NodeXML.class).needType()) {
				ch.setAttribute(ATR_TYPE_VALUE, v.getClass().getName());
			}
			fillElem(v, ch, null);
		}
	}
	
	private void fillElem(Object v, ElemXML el, Field fieldForThisValue) throws IllegalArgumentException, SecurityException, UtilXMLException, IllegalAccessException, InvocationTargetException, NoSuchMethodException{
		if(v==null) {
			return;
		}
		if(v instanceof Map){
			if(fieldForThisValue==null && v!=null) { //) || fieldForThisValue.getAnnotation(NodeXML.class).needType() // precisa?
				el.setAttribute(ATR_TYPE_MAP, v.getClass().getName());// para conseguir gerar com o tipo certo na hora de ler o XML caso não tenha o field com os tipos ...
			}
			Map m = (Map)v;
			if(m.size()>0 || isAddEmptys()) {
				fillElemMap(m, el, fieldForThisValue);
			}
		}else if(v instanceof Collection){
			if(fieldForThisValue==null && v!=null) { //) || fieldForThisValue.getAnnotation(NodeXML.class).needType() // precisa?
				el.setAttribute(ATR_TYPE_LIST, v.getClass().getName());// para conseguir gerar com o tipo certo na hora de ler o XML caso não tenha o field com os tipos ...
			}
			Collection l = (Collection)v;
			if(l.size()>0 || isAddEmptys()) {
				fillElemList(l, el, fieldForThisValue);
			}
		}else{
			if(!v.getClass().isAnnotationPresent(NodeXML.class)){// se não tiver a ann NodeXML será um valor para converter em String
				boolean useCdata = false;
				if(fieldForThisValue!=null){
					//opção de forçar setar tipo na tag para caso no obj tenha algum tipo generico como Object ...
					NodeXML ann = fieldForThisValue.getAnnotation(NodeXML.class);
					if(ann!=null && ann.needType() && !fieldForThisValue.getType().equals(v.getClass())) { // se tem o needType, mas o tipo do objeto é igual do field, não precisa setar o tipo
						el.setAttribute(ATR_TYPE_VALUE, v.getClass().getName());
					}
					useCdata = ann!=null && ann.useCdata();
				}
				if(useCdata) el.setTextCdata(UtilStrObj.toStr(v));
				else el.setText(UtilStrObj.toStr(v)); // se não tem annotation deve ser um tipo básico
			}else{
				fillElemObj(v, el);// aqui não precisa do tipo, pois o tipo será o próprio obj
			}
		}
		// se for map verificar se tem algum atributo além do key 
		boolean isItemMap = TAG_ITEM.equals(el.getNodeName()) && el.getAttribute(ATR_KEY, true)!=null;
		boolean hasSomeAttribute = el.hasSomeAttribute() && (!isItemMap || el.getAttributesCount()>1); // se não for map ou se for map e tiver mais de um atributo ...
		//System.out.println("xxx>>>"+el.getNodeName()+"|"+isAddEmptys()+"|"+el.isRoot()+"|"+el.getText(false)+"|"+hasSomeAttribute+"|"+el.getChildsCount()+"|"+isItemMap);
		if(!isAddEmptys() && !el.isRoot() && el.getText(false)==null && !hasSomeAttribute && el.getChildsCount()==0){
			el.removerThis();// remove a tag se não tem valor nenhum nem filhos
		}
	}

	private Class<?> getTypeTag(ElemXML el, String atr) throws ClassNotFoundException, UtilXMLException{
		String t = el.getAttribute(atr, true);
		if(t==null) throw new UtilXMLException("Type not found for tag without field. Tag: "+el.getNodeName());
		else return Class.forName(t);
	}
	
	// o elem deve ser o root de uma lista de itens
	private Map readElemMap(ElemXML el, Field fieldMap, Object instObjFather) throws UtilXMLException, SecurityException, InstantiationException, IllegalAccessException, NoSuchFieldException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, ParseException, ClassNotFoundException, APIBaseException, IOException{
		//MapProps m = new MapProps();
		Class typeMap;
		if(fieldMap!=null){
			typeMap = fieldMap.getType();
		}else{
			typeMap = getTypeTag(el,ATR_TYPE_MAP);
		}
		Map m = (Map)typeMap.newInstance();// os tipos dos fields não podem ser interfaces ou abstratos ou com params no construtor
		
		Class<?> typeKeyMap = null;
		Class<?> typeValueMap = null;
		if(fieldMap!=null){
			Class<?>[] typesEntryMap = getGenericTypes(fieldMap);
			typeKeyMap = typesEntryMap[0];
			typeValueMap = typesEntryMap[1];
		}
		boolean temTiposDefinidos = typeKeyMap!=null && typeKeyMap!=null;
		for(ElemXML elItemMap : el.getElementsByTagName(getNameTagForItemOfListOrMap(fieldMap), true)){
			if(!temTiposDefinidos){// ???? pode ser uma Map com tipos genéricos ???
				typeKeyMap = getTypeTag(elItemMap, ATR_TYPE_KEY);
				typeValueMap = getTypeTag(elItemMap, ATR_TYPE_VALUE);
			}
			String kStr = elItemMap.getAttribute(ATR_KEY, true);
			if(kStr==null) throw new UtilXMLException("Attribute key not found for read Map");
			Object k = UtilStrObj.toObj(kStr, typeKeyMap);
			// se for lista ou map, passar o obj pai da lista para ser o pai do item
			m.put(k, readElem(elItemMap, typeValueMap, null, instObjFather));// itens da lista devem ser tipos básicos ou novas classes, não usar maps ou listas aninhadas
		}
		return m;
	}
	// o elem deve ser o root de uma lista de itens
	private Collection readElemList(ElemXML el, Field fieldList, Object instObjFather) throws UtilXMLException, InstantiationException, IllegalAccessException, SecurityException, NoSuchFieldException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, ParseException, ClassNotFoundException, APIBaseException, IOException{
		//ArrayList a = new ArrayList();
		Class typeList;
		if(fieldList!=null){
			typeList = fieldList.getType();
		}else{
			typeList = getTypeTag(el,ATR_TYPE_LIST);
		}
		Collection a = (Collection)typeList.newInstance(); // os tipos dos fields não podem ser interfaces ou abstratos ou com params no construtor
		//Field fieldList = getFieldForTag(el.getNodeName(), VER_COMO_TER_A_CLASSE_PAI_DO_LIST);
		Class<?> typeItemList = null;
		if(fieldList!=null){
			typeItemList = getGenericTypes(fieldList)[0];
		}
		boolean temTiposDefinidos = typeItemList!=null;
		for(ElemXML elItemList : el.getElementsByTagName(getNameTagForItemOfListOrMap(fieldList), true)){
			if(!temTiposDefinidos){
				typeItemList = getTypeTag(elItemList, ATR_TYPE_VALUE); // ??? pode ter diferentes tipos para cada item da lista (pode ser uma lista genérica)
			}
			// se for lista ou map, passar o obj pai da lista para ser o pai do item
			a.add(readElem(elItemList, typeItemList, null, instObjFather));// itens da lista devem ser tipos básicos ou novas classes, não usar maps ou listas aninhadas
		}
		return a;
	}
	private <T> T readElemObj(ElemXML elRead, Class<T> typeObj, Object instObjFather) throws IllegalArgumentException, IllegalAccessException, UtilXMLException, InstantiationException, InvocationTargetException, SecurityException, NoSuchMethodException, NoSuchFieldException, ParseException, ClassNotFoundException, APIBaseException, IOException{
		
		Class<T> typeForNewInst = getTypeForInstantiate(elRead, typeObj);
		
		if(!typeForNewInst.isAnnotationPresent(NodeXML.class)){// se não tiver a ann NodeXML será um valor para converter em String
			//opção de forçar setar tipo na tag para caso no obj tenha algum tipo generico como Object ...
			return UtilStrObj.toObj( elRead.getText(false) , typeForNewInst ); // se não tem annotaion deve ser um tipo básico
		}else{
			//validateAnnClass(typeObj);// se não for tipo básico deve ser uma classe criada e com annotation NodeXMl
			
			T obj = newInst( typeForNewInst, instObjFather);
			
			Class<?> c = typeForNewInst;
			while(c.isAnnotationPresent(NodeXML.class)){
				for(Field f : c.getDeclaredFields()){
					if(f.isAnnotationPresent(NodeXML.class)){
						NodeXML ann = f.getAnnotation(NodeXML.class);
						if(ann.asAttribute()) {
							String valAtribString = elRead.getAttribute(getNameTagForField(f), true);
							if(valAtribString==null) continue;
							UtilReflectionField urf = new UtilReflectionField(f);
							urf.setValueInObject(obj, UtilStrObj.toObj(valAtribString,f.getType()) );
						}else{
							ElemXML elItemObj = elRead.getFirstElemByTagName(getNameTagForField(f), false, true);
							if(elItemObj==null) continue;
							UtilReflectionField urf = new UtilReflectionField(f);
							urf.setValueInObject(obj, readElem(elItemObj, f.getType(), f, obj));
						}
					}
				}
				c = c.getSuperclass();
			}
			return obj;
		}
	}
	
	private <T> T readElem(ElemXML item, Class<T> typeObjItem, Field fieldItem, Object instObjFather) throws SecurityException, UtilXMLException, InstantiationException, IllegalAccessException, NoSuchFieldException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, ParseException, ClassNotFoundException, APIBaseException, IOException{
		//Class<T> typeObjItem = (Class<T>)fieldItem.getType();
		if(Map.class.isAssignableFrom(typeObjItem)){
			return (T)readElemMap(item,fieldItem, instObjFather);
		}else if(Collection.class.isAssignableFrom(typeObjItem)){
			return (T)readElemList(item,fieldItem, instObjFather);
		}else{
			return readElemObj(item, typeObjItem, instObjFather);
		}
	}
	
	
	private <T> Class<T> getTypeForInstantiate(ElemXML elRead, Class<T> defaultType) throws ClassNotFoundException{
		String typeValue = elRead.getAttribute(ATR_TYPE_VALUE, true);
		if(typeValue!=null){
			return (Class<T>)Class.forName(typeValue); // o tipo deve extender o default
		}else{
			return defaultType;
		}
	}
	
	private <T> T newInst(Class<T> typeObj, Object instObjFather) throws InstantiationException, IllegalAccessException, IllegalArgumentException, SecurityException, InvocationTargetException{
		if(instObjFather!=null){
			// se receber a instancia o obj pai, tenta chamar o construtor que recebe este objeto pai (para classes que tenham parent ...)
			for(Constructor<?> c : typeObj.getConstructors()){
				if(c.getParameterTypes().length==1 && c.getParameterTypes()[0].isAssignableFrom(instObjFather.getClass())){
					return (T)c.newInstance(instObjFather);
				}
			}
		}
		return typeObj.newInstance();// deve ter construtor vazio // os tipos dos fields não podem ser interfaces ou abstratos ou com params no construtor
	}
	
	
	// RETORNA A CLASSE DO PARAM GENERICS QUE TIVER ANNOTATION ENTITY
	private Class<?>[] getGenericTypes(Field field){
		Type t = field.getGenericType();
		if ( !(t instanceof ParameterizedType) ) return null; 
		ParameterizedType paramType = (ParameterizedType)t;
		Type[] args = paramType.getActualTypeArguments();
		Class<?>[] resp = new Class<?>[args.length];
		for(int i=0;i<resp.length;i++){
			//System.out.println("aaaa>"+args[i]);
			if(args[i] instanceof ParameterizedTypeImpl){
				ParameterizedTypeImpl p = (ParameterizedTypeImpl)args[i];
				resp[i] = (Class<?>)p.getRawType();
			}else{
				resp[i] = (Class<?>)args[i];
			}
		}
		return resp;
	}
	
	public boolean isAddEmptys() {
		return addEmptys;
	}
	public void setAddEmptys(boolean addEmptys) {
		this.addEmptys = addEmptys;
	}
	
}
